﻿using System;
using System.ComponentModel.Composition;
using System.Security.Cryptography;
using System.Threading.Tasks;
using System.IO;
using NthDownload.Common;
using NthDownload.Contracts;
using System.Collections.Generic;
using System.Text;

namespace NthDownload.BL.Persistence
{
    [PartCreationPolicy(CreationPolicy.Shared)]
    [Export(typeof(IKeyValuePairPersistenceService))]
    public class FileKeyValuePairPersistenceService : IKeyValuePairPersistenceService
    {
        #region Class Members

        private static readonly Dictionary<string, object> KeySyncObjects = new Dictionary<string, object>();
        private static readonly object SyncObject = new object();

        #endregion Class Members

        #region Methods

        /// <summary>
        /// Save value for key
        /// </summary>
        public Task Save(string key, string value)
        {
            Task saveTask = new Task(() =>
            {
                //
                // Get sync object for key
                //
                object keySyncObject;
                lock (SyncObject)
                {
                    if (!KeySyncObjects.TryGetValue(key, out keySyncObject))
                    {
                        keySyncObject = new object();
                        KeySyncObjects.Add(key, keySyncObject);
                    }
                }

                //
                // Save data
                //
                lock (keySyncObject)
                {
                    string fileName = CreateFileName(key);
                    string filePath = CreateFilePath(fileName);
                    string newFilePath = CreateNewFilePath(fileName);
                    string oldFilePath = CreateOldFilePath(fileName);

                    WriteNewFile(value, newFilePath);
                    ReplaceCurrentFileWithNew(oldFilePath, filePath, newFilePath); 
                }
            });

            saveTask.Start();
            return saveTask;
        }

        /// <summary>
        /// Load value for key
        /// </summary>
        public Task<string> Load(string key)
        {
            Task<string> loadTask = new Task<string>(() =>
            {
                //
                // Get sync object for key
                //
                object keySyncObject;
                lock (SyncObject)
                {
                    if (!KeySyncObjects.TryGetValue(key, out keySyncObject))
                    {
                        keySyncObject = new object();
                        KeySyncObjects.Add(key, keySyncObject);
                    }
                }

                //
                // Load data
                //
                lock (keySyncObject)
                {
                    string fileName = CreateFileName(key);
                    string filePath = CreateFilePath(fileName);
                    string newFilePath = CreateNewFilePath(fileName);
                    string oldFilePath = CreateOldFilePath(fileName);

                    if (CheckFileExists(oldFilePath, filePath, newFilePath))
                    {
                        return ReadFile(filePath);
                    }

                    return null;
                }
            });

            loadTask.Start();
            return loadTask;
        }

        #endregion Methods

        #region Private Methods

        /// <summary>
        /// Reads data form a file
        /// </summary>
        private string ReadFile(string filePath)
        {
            FileInfo file = new FileInfo(filePath);
            FileStream fileStream = file.OpenRead();

            StreamReader streamReader = new StreamReader(fileStream);
            string data = streamReader.ReadToEnd();
            streamReader.Close();
            return data;
        }

        /// <summary>
        /// Checks if a file exists, if it doesn't it will check for new and old files
        /// </summary>
        private bool CheckFileExists(string oldFilePath, string currentFilePath, string newFilePath)
        {
            FileInfo currentFile = new FileInfo(currentFilePath);

            if (!currentFile.Exists)
            {
                FileInfo newFile = new FileInfo(newFilePath);
                FileInfo oldFile = new FileInfo(oldFilePath);

                if (newFile.Exists)
                {
                    newFile.MoveTo(currentFilePath);
                }
                else if (oldFile.Exists)
                {
                    oldFile.MoveTo(currentFilePath);
                }
                else
                {
                    return false;
                }
            }

            return true;
        }

        /// <summary>
        /// Writes profile data to a new file
        /// </summary>
        private void WriteNewFile(string data, string newFilePath)
        {
            FileStream fileStream = new FileStream(newFilePath, FileMode.Create, FileAccess.Write);
            StreamWriter streamWriter = new StreamWriter(fileStream);
            streamWriter.Write(data);
            streamWriter.Close();
        }

        /// <summary>
        /// Replaces the current file with a newly written file
        /// </summary>
        private void ReplaceCurrentFileWithNew(string oldFilePath, string currentFilePath, string newFilePath)
        {
            FileInfo oldFile = new FileInfo(oldFilePath);
            if (oldFile.Exists)
            {
                oldFile.Delete();
            }

            FileInfo currentFile = new FileInfo(currentFilePath);
            FileInfo newFile = new FileInfo(newFilePath);
            if (currentFile.Exists && newFile.Exists)
            {
                currentFile.MoveTo(oldFilePath);
            }

            if (newFile.Exists)
            {
                newFile.MoveTo(currentFilePath);
            }

            oldFile = new FileInfo(oldFilePath);
            if (oldFile.Exists)
            {
                oldFile.Delete();
            }
        }

        /// <summary>
        /// Creates a file name for a key
        /// </summary>
        private string CreateFileName(string key)
        {
            ASCIIEncoding encoder = new ASCIIEncoding();
            byte[] keyHash = SHA1.Create().ComputeHash(encoder.GetBytes(key));
            return string.Format("{0}.{1}", BitConverter.ToString(keyHash).Replace("-", string.Empty), "xml");
        }

        /// <summary>
        /// Creates path to a file in the data directory
        /// </summary>
        private string CreateFilePath(string fileName)
        {
            return Path.Combine(Paths.DataPath, fileName);
        }

        /// <summary>
        /// Creates path to a file in the data directory with an added '.new' extension
        /// </summary>
        private string CreateNewFilePath(string fileName)
        {
            return Path.Combine(Paths.DataPath, fileName) + ".new";
        }

        /// <summary>
        /// Creates path to a file in the data directory with an added '.old' extension
        /// </summary>
        private string CreateOldFilePath(string fileName)
        {
            return Path.Combine(Paths.DataPath, fileName) + ".old";
        }

        #endregion Private Methods
    }
}
